#include <errno.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include "gc.h"
#include "thread.h"
#include "bytecode.h"
#include "redcodeobject.h"
#include "rednullobject.h"
#include "redarrayobject.h"
#include "redframeobject.h"
#include "redtraceobject.h"
#include "redstringobject.h"
#include "redexceptionobject.h"

static RedObject *exception_repr(RedObject *self);

static void exception_clear(RedObject *self)
{
	Red_UNREF(RedException_Message(self));
	Red_UNREF(RedException_CallStack(self));
}

static void exception_release(RedObject *self)
{
	exception_clear(self);
	Red_RELEASE();
}

static void exception_traverse(RedObject *self, tp_visit_fn_t visit, void *args)
{
	Red_VISIT(RedException_Message(self));
	Red_VISIT(RedException_CallStack(self));
}

static RedObject *exception_delattr(RedObject *self, RedObject *attr)
{
	RedException_ThrowFormat(&RedExc_AttributeError, "Cannot __delattr__ from exception objects");
	return NULL;
}

static RedObject *exception_getattr(RedObject *self, RedObject *attr)
{
	if (RedString_EqualsCString(attr, "message"))
		return RedType_Object.tp_getattr(self, attr);

	Red_REF(RedException_Message(self));
	return RedException_Message(self);
}

static RedObject *exception_setattr(RedObject *self, RedObject *attr, RedObject *value)
{
	RedException_ThrowFormat(&RedExc_AttributeError, "Cannot __setattr__ to exception objects");
	return NULL;
}

RedTypeObject RedExc_Exception =
{
	RedType_INIT("Exception", RedExceptionObject, RedType_Object)

	.tp_str			= exception_repr,
	.tp_repr		= exception_repr,
	.tp_clear		= exception_clear,
	.tp_release		= exception_release,
	.tp_traverse	= exception_traverse,

	.tp_delattr		= exception_delattr,
	.tp_getattr		= exception_getattr,
	.tp_setattr		= exception_setattr,
};

#define DefineError(name)											\
	RedTypeObject RedExc_##name =									\
	{																\
		RedType_INIT(#name, RedExceptionObject, RedExc_Exception)	\
	}

DefineError(SystemExit);
DefineError(KeyboardInterrupt);

DefineError(IOError);
DefineError(OSError);
DefineError(NameError);
DefineError(TypeError);
DefineError(IndexError);
DefineError(ValueError);
DefineError(FormatError);
DefineError(ImportError);
DefineError(MemoryError);
DefineError(CompileError);
DefineError(RuntimeError);
DefineError(ArgumentError);
DefineError(InternalError);
DefineError(AttributeError);
DefineError(FloatPointError);
DefineError(UnhashableError);
DefineError(NotCallableError);
DefineError(NotPrintableError);
DefineError(RecursionTooDepthError);

#undef DefineError

static RedExceptionObject oom_instance =
{
	RedObject_HEAD_INIT(RedExc_MemoryError)

	.ex_pos			= -1,
	.ex_line		= -1,
	.ex_message		= NULL,
	.ex_callstack	= NULL,
};

static RedObject *exception_repr(RedObject *self)
{
	/* we can do almost nothing when out-of-memory exception occured.
	   this is an fatal exception, but user can catch it and free some memory.
	   it proves that user doesn't do that when control flow reaches here.
	   the only thing we can do is print an exception message on screen and abort(). */
	if (self == (RedObject *)(&oom_instance))
	{
		fprintf(stderr, "*** FATAL: Out of memory\n");
		abort();
	}

	RedObject *msg = RedException_Message(self);
	RedObject *repr = RedString_FromCString("Traceback (most recent call last):\n");

	for (int i = RedArray_Length(RedException_CallStack(self)) - 1; i >= 0; i--)
	{
		RedObject *trace = RedArray_Items(RedException_CallStack(self))[i];

		/* extract file name and source code if any */
		RedObject *fname = RedTrace_Fname(trace);
		RedObject *module = RedTrace_Module(trace);
		RedObject *source = RedTrace_Line(trace) ? RedString_Strip(RedTrace_Line(trace)) : RedString_FromCString("<no source>");
		RedObject *message = RedString_FromCFormat(
		   "  File '%s', line %d, module '%s'\n    %s\n",
		   RedString_Buffer(fname),
		   RedTrace_LineNo(trace),
		   RedString_Buffer(module),
		   RedString_Buffer(source));

		/* append to result */
		RedObject *newstring = RedString_Append(repr, message);

		Red_UNREF(repr);
		Red_UNREF(source);
		Red_UNREF(message);

		/* replace with new string */
		repr = newstring;
	}

	RedObject *result = NULL;

	if (!msg || RedNull_IsNull(msg))
		result = RedString_AppendCFormat(repr, "Exception: %s\n", RedObject_ClassName(self));
	else
		result = RedString_AppendCFormat(repr, "Exception: %s(%s)\n", RedObject_ClassName(self), RedString_Buffer(msg));
	
	Red_UNREF(repr);
	return result;
}

RedObject *RedException_Exception(void)
{
	RedThread *thread;
	return (thread = RedThread_Self()) ? thread->exception : NULL;
}

void RedException_Throw(RedTypeObject *exception)
{
	Red_REF(Red_Null);
	RedException_ThrowObject(exception, Red_Null);
}

void RedException_ThrowErrno(RedTypeObject *exception)
{
	RedException_ThrowFormat(exception, "[errno %d] %s", errno, strerror(errno));
}

void RedException_ThrowObject(RedTypeObject *exception, RedObject *object)
{
	RedThread *thread = RedThread_Self();

	/* exceptions outside RedThread is not recoverable */
	if (!thread)
	{
		fprintf(stderr, "*** FATAL: exceptions outside RedThread is not recoverable\n");
		abort();
	}

	/* another exception occured when processing this exception */
	if (thread->throwing)
	{
		fprintf(stderr, "*** FATAL: another exception occured when processing this exception\n");
		abort();
	}

	/* unref the previous exception */
	Red_UNREF(thread->exception);
	ATOMIC_INC(thread->throwing);

	/* cannot even allocate memory, we can only use the pre-defined instance */
	if (exception == &RedExc_MemoryError)
	{
		thread->exception = (RedObject *)(&oom_instance);
		Red_REF(thread->exception);
		ATOMIC_DEC(thread->throwing);
		return;
	}

	/* not an memory error, record it */
	thread->exception = RedObject_Create(exception, NULL);

	Red_REF(object);
	RedException_Message(thread->exception) = object;
	RedException_CallStack(thread->exception) = RedArray_Create();

	RedObject_GC_Insert(thread->exception);
	RedObject_GC_Insert(RedException_CallStack(thread->exception));

	/* leaving throw function */
	ATOMIC_DEC(thread->throwing);
}

void RedException_ThrowFormat(RedTypeObject *exception, const char *format, ...)
{
	va_list args;
	va_start(args, format);
	RedObject *object = RedString_FromCFormatVa(format, args);

	if (!object)
	{
		fprintf(stderr, "*** FATAL: another exception occured when processing this exception\n");
		abort();
	}

	RedException_ThrowObject(exception, object);
	Red_UNREF(object);
}

void RedException_Clear(void)
{
	RedThread *thread;
	if ((thread = RedThread_Self()))
		Red_UNREF(thread->exception);
}

void RedException_Rewind(RedObject *frame)
{
	RedThread *thread = RedThread_Self();

	/* exceptions outside RedThread is not recoverable */
	if (!thread)
	{
		fprintf(stderr, "*** FATAL: exceptions outside RedThread is not rewind-able\n");
		abort();
	}

	/* out-of-memory exception is not rewind-able */
	if (thread->exception != (RedObject *)(&oom_instance))
	{
		int pc = RedFrame_PC(frame) - 1;
		RedObject *code = RedFrame_Code(frame);
		bytecode_t *opcodes = RedCode_Buffer(code);

		/* extract source line */
		RedObject *line = (RedCode_Source(code) && opcodes[pc].lineno >= 0) ? RedArray_Items(RedCode_Source(code))[opcodes[pc].lineno] : NULL;

		/* create traceback object */
		RedObject *array = RedException_CallStack(thread->exception);
		RedObject *trace = RedTrace_Traceback(opcodes[pc].lineno, opcodes[pc].position, line, RedCode_Fname(code), RedCode_Module(code));

		if (!trace)
		{
			fprintf(stderr, "*** FATAL: another exception occured rewinding\n");
			abort();
		}

		/* append to stack trace */
		RedArray_Append(array, trace);
		Red_UNREF(trace);
	}
}
